<head>
  <style> body { margin: 0; } </style>

  <script src="//bundle.run/@yarnpkg/lockfile@1.1.0"></script>

  <script src="//unpkg.com/force-graph"></script>
<!--  <script src="../../dist/force-graph.js"></script>-->
</head>

<body>
  <div id="graph"></div>

  <script type="module">
    import dagre from 'https://esm.sh/dagre';
    import accessorFn from 'https://esm.sh/accessor-fn';

    const Graph = new ForceGraph(document.getElementById('graph'))
      .nodeId('id')
      .nodeLabel('id')
      .cooldownTicks(0) // pre-defined layout, cancel force engine iterations
      .linkDirectionalArrowLength(3)
      .linkDirectionalArrowRelPos(1)
      .linkCurvature(d =>
        0.07 * // max curvature
        //   curve outwards from source, using gradual straightening within a margin of a few px
        Math.max(-1, Math.min(1, (d.source.x - d.target.x) / 5)) *
        Math.max(-1, Math.min(1, (d.target.y - d.source.y) / 5))
      );

    fetch('../../yarn.lock')
      .then(r => r.text())
      .then(text => {
        const yarnlock = _yarnpkg_lockfile.parse(text);
        if (yarnlock.type !== 'success') throw new Error('invalid yarn.lock');
        return yarnlock.object;
      })
      .then(yarnlock => {
        const nodes = [];
        const links = [];
        Object.entries(yarnlock).forEach(([pkg, details]) => {
          nodes.push({ id: pkg });
          if (details.dependencies) {
            Object.entries(details.dependencies).forEach(([dep, version]) => {
              links.push({source: pkg, target: `${dep}@${version}`});
            });
          }
        });
        return { nodes, links };
      }).then(data => {
        const nodeDiameter = Graph.nodeRelSize() * 2;
        const layoutData = getLayout(data.nodes, data.links, {
          nodeWidth: nodeDiameter,
          nodeHeight: nodeDiameter,
          nodesep: nodeDiameter * 0.5,
          ranksep: nodeDiameter * Math.sqrt(data.nodes.length) * 0.6,

          // root nodes aligned on top
          rankDir: 'BT',
          ranker: 'longest-path',
          linkSource: 'target',
          linkTarget: 'source'
        });
        layoutData.nodes.forEach(node => { node.fx = node.x; node.fy = node.y; }); // fix nodes

        Graph.graphData(layoutData);
        Graph.zoomToFit();
      });

    //

    function getLayout(nodes, links, {
      nodeId = 'id',
      linkSource = 'source',
      linkTarget = 'target',
      nodeWidth = 0,
      nodeHeight = 0,
      ...graphCfg
    } = {}) {
      const getNodeWidth = accessorFn(nodeWidth);
      const getNodeHeight = accessorFn(nodeHeight);

      const g = new dagre.graphlib.Graph();
      g.setGraph({
        // rankDir: 'LR',
        // ranker: 'network-simplex' // 'tight-tree', 'longest-path'
        // acyclicer: 'greedy'
        nodesep: 5,
        edgesep: 1,
        ranksep: 20,
        ...graphCfg
      });

      nodes.forEach(node =>
        g.setNode(
          node[nodeId],
          Object.assign({}, node, {
            width: getNodeWidth(node),
            height: getNodeHeight(node)
          })
        )
      );
      links.forEach(link =>
        g.setEdge(link[linkSource], link[linkTarget], Object.assign({}, link))
      );

      dagre.layout(g);

      return {
        nodes: g.nodes().map(n => {
          const node = g.node(n);
          delete node.width;
          delete node.height;
          return node;
        }),
        links: g.edges().map(e => g.edge(e))
      };
    }
  </script>
</body>
